use std::error::Error;
use std::str::FromStr;

use crate::param::ParamAppender;
use apikit_core::types::Method;
use async_trait::async_trait;
use reqwest::Response;
use reqwest::{Client, Url};
use thiserror::Error;

#[async_trait]
pub trait HttpClient {
    type Error: Error;
    type ParamBuilder;
    type Response;

    async fn call_service<Param, Output>(
        &self,
        url: &str,
        method: Method,
        param: Param,
    ) -> Result<Output, Self::Error>
    where
        Param: Send + Sync + 'static,
        Param: ParamAppender<Self::ParamBuilder>,
        Output: Send + Sync + 'static,
        Output: FromResponse<Response>;
}

#[async_trait]
pub trait FromResponse<Response>: Sized {
    type Error: Error;

    async fn from_response(response: Response) -> Result<Self, Self::Error>;
}

#[derive(Debug, Error)]
pub enum ReqwestCallError {
    #[error("invalid method [{0}]")]
    InvalidMethod(String),

    #[error("invalid url [{0}]")]
    InvalidUrl(String),

    #[error("{0}")]
    AppendParamError(Box<dyn Error>),

    #[error(transparent)]
    ReqwestError(#[from] reqwest::Error),

    #[error("{0}")]
    FromResponseError(Box<dyn Error>),
}

#[async_trait]
impl HttpClient for Client {
    type Error = ReqwestCallError;
    type ParamBuilder = reqwest::RequestBuilder;
    type Response = Response;

    async fn call_service<Param, Output>(
        &self,
        url: &str,
        method: Method,
        param: Param,
    ) -> Result<Output, Self::Error>
    where
        Param: Send + Sync + 'static,
        Output: Send + Sync + 'static,
        Param: ParamAppender<reqwest::RequestBuilder>,
        Output: FromResponse<Response>,
    {
        let builder: reqwest::RequestBuilder = self.request(
            method,
            Url::from_str(url).map_err(|_| ReqwestCallError::InvalidUrl(url.to_string()))?,
        );

        let builder = param
            .append_param(builder)
            .map_err(|e| ReqwestCallError::AppendParamError(Box::new(e)))?;
        let response = builder.send().await?;
        let output = Output::from_response(response)
            .await
            .map_err(|e| ReqwestCallError::FromResponseError(Box::new(e)))?;

        Ok(output)
    }
}

#[async_trait]
impl FromResponse<reqwest::Response> for String {
    type Error = reqwest::Error;

    async fn from_response(response: Response) -> Result<Self, Self::Error> {
        response.text().await
    }
}

#[async_trait]
impl FromResponse<reqwest::Response> for Response {
    type Error = reqwest::Error;

    async fn from_response(response: Response) -> Result<Self, Self::Error> {
        Ok(response)
    }
}
